//
//  WebExtraction.m
//  WebClipper
//
//  Created by Daniel Braun on 24/02/07.
//  Copyright 2007 Daniel Braun http://page-appart.net, http://braun.daniel.free.fr . All rights reserved.
//

#import "WCWebExtractionPath.h"
#import "StringExtension.h"
#import "CalendarRange.h"

@implementation _WebExtractionPathItem

- (NSString *) description
{
	if (!active) return @"_WebExtractionPathItem, inactive";
	return [NSString stringWithFormat:@"_WebExtractionPathItem %s node=%@ id=%@ class=%@ spec=%d occ=%d samelev=%@ string=%@ %@",
		active ? "" : "(inact)",
		     nodeName ? nodeName : @"n/a",
			 idName ? idName : @"n/a",
		      classNam ? classNam: @"n/a",
		specialAction, occurence,
		sameLevel ? @"YES":@"no",
		(specialAction==4) ? matchString : @"n/a",
		match ? @" (matched)":@""];
}
-(void) dealloc {
	[nodeName release];
	[idName release];
	[classNam release];
	[parent release];
	[domNode release];
	[matchString release];
	[super dealloc];
}

- (id) init {
	self = [super init];
	if (self != nil) {
		occurence=-1;
		active=YES; sameLevel=YES; activeCanBeChanged=YES;
	}
	return self;
}


- (id) initWithCoder:(NSCoder *) coder
{
	//self=[super initWithCoder:coder];
	self=[super init];
	occurence=-1;
	if ([coder allowsKeyedCoding]) {
		nodeName=[[[coder decodeObjectForKey:@"nodeName"]lowercaseString]retain];
		idName=[[[coder decodeObjectForKey:@"idName"]lowercaseString]retain];
		matchString=[[coder decodeObjectForKey:@"matchString"]retain];
		classNam=[[[coder decodeObjectForKey:@"classNam"]lowercaseString]retain];
		occurence=[coder decodeIntForKey:@"occurence"];
		active=[coder decodeBoolForKey:@"active"];
		sameLevel=[coder decodeBoolForKey:@"sameLevel"];
		activeCanBeChanged=[coder decodeBoolForKey:@"activeCanBeChanged"];
		specialAction=[coder decodeIntForKey:@"specialAction"];
	}
	return self;
}

- (void)encodeWithCoder:(NSCoder *)coder
{
	//[super encodeWithCoder:coder];
	if ( [coder allowsKeyedCoding] ) {
		[coder encodeObject:nodeName forKey:@"nodeName"];
		[coder encodeObject:matchString forKey:@"matchString"];
		[coder encodeObject:idName forKey:@"idName"];
		[coder encodeObject:classNam forKey:@"classNam"];
		[coder encodeInt:occurence forKey:@"occurence"];
		[coder encodeBool:active forKey:@"active"];
		[coder encodeBool:sameLevel forKey:@"sameLevel"];
		[coder encodeBool:activeCanBeChanged forKey:@"activeCanBeChanged"];
		[coder encodeInt:specialAction forKey:@"specialAction"];
	}
}

- (void) updated
{
	if ([self respondsToSelector:@selector(_updated)]) {
		[(id)self _updated];
	}
}

- (NSString *) jsCode
{
	NSString *z=[[NSString stringFromResourceFile:@"js.3"] 
		parseWithActions:nil
			     for:self andSimpleReplacement:[NSDictionary dictionaryWithObjectsAndKeys:
					nodeName ? [nodeName lowercaseString] : @"", @"tag",
					idName ? [idName lowercaseString] : @"", @"idname",
					classNam ? [classNam lowercaseString]: @"", @"classname", 
				     [[self noccurence] stringValue], @"n", nil ]];
	return z;
}

- (NSNumber *) noccurence
{
	return [NSNumber numberWithInt:occurence];
}
- (void) setOccurence:(int)n
{
	[self updated];
	occurence=n;
}
- (void) setNoccurence:(NSNumber *)n
{
	[self updated];
	[self setOccurence:[n intValue]];
}

static BOOL containMonth(NSString *s, int moff);

- (BOOL) matches:(DOMNode *)n
{
	DOMHTMLElement *e=(DOMHTMLElement *)n;
	
	
	NSString *nn=[[e nodeName]lowercaseString];
	//NSLog(@" for %@/%@/%@ test %@\n", nodeName, idName, classNam, nn);
	NSAssert(nodeName, @"should have a nodename");
	if (![nodeName isEqualToString:nn]) return NO;
	if ([nodeName isEqualToString:@"#text"]) goto matchok; // for DOM text, simply match on nodename
	
	NSString *idn=[[e idName]lowercaseString];
	NSString *cn=[[e className]lowercaseString];
	
	if (classNam && ![classNam isEqualToString:cn]) return NO;
	if (idName && ![idName isEqualToString:idn]) return NO;
	NSString *si=nil;
	NSRange r;
	CalendarRange *cr;
matchok:
	if ([e respondsToSelector:@selector(innerText)]) si=[[e innerText] lowercaseString];
	else if ([e respondsToSelector:@selector(stringValue)]) si=[[(id)e stringValue]lowercaseString];
	switch (specialAction) {
		case 0: break;
		default: break;
		case 1: /* contain month */
			if (containMonth(si,0)) return YES;
			return NO;
		case 2: /* contain day */
			r=[si firstNumberRange];
			if (!r.length) return NO;
			si=[si substringWithRange:r];
			int d=[si intValue];
			if (d<0) return NO;
			if (d>31) return NO;
			NSCalendarDate *cd=[NSCalendarDate date];
			int dm=[cd dayOfMonth];
			if (d<dm) return NO;
			break;
		case 3: /* contain current date */
			cr=[[CalendarRange alloc]initWithNaturalLangageString:si];
			if (!cr) return NO;
			BOOL r=[cr containDate:[NSCalendarDate date]];
			return r;
			break;
		case 4: /* match string */
			if (matchString) {
				if ([[si lowercaseString] hasSubString:[matchString lowercaseString]]) {
					return YES;
				}
			}
			return NO;
			break;
		case 5: /* near string */
			break;
			
	}
	return YES;
}


- (NSArray *) findMatchingFrom: (DOMNode *)dn sameLevel:(BOOL)sl 
		    lookingFor:(DOMNode *)node 
		       counter:(int)pc returnIndex:(int *)pidx
{
	//NSAssert(-1==occurence, @"hu?");
	DOMNode *n;
	int cnt=pc;
	NSMutableArray *res=[NSMutableArray arrayWithCapacity:20];
	//NSLog(@"findMatchingFrom\n");
	
	for (n=[dn firstChild]; n; n=[n nextSibling]) {
		if ([self matches:n]) {
			[res addObject:n];
			if ((n==node) && pidx) *pidx=cnt;
			cnt++;
		}
		if (!sl) {
			NSArray *r=[self findMatchingFrom:n sameLevel:NO
					       lookingFor:node counter:cnt returnIndex:pidx];
			cnt+=[r count];
			[res addObjectsFromArray:r];
		}
	}
	if (!cnt) {
		//NSLog(@"nil count here\n");
	}
	return res;
}

- (DOMHTMLElement *)extractFrom:(DOMHTMLElement *)n
{
	int occ=occurence;
	if (specialAction) occ=0;
	if (!active) return n;
	if ([nodeName isEqualToString:@"tbody"]) {
		NSLog(@"tbody\n");
	}
	//NSLog(@"---- matching\n");
	NSArray *r=[self findMatchingFrom:n sameLevel:sameLevel
			       lookingFor:nil
				  counter:0 returnIndex:nil];
	if ([r count]<=occ) {
		//NSLog(@"not found\n");
		return nil;
	}
	//NSLog(@"ok, found\n");
	match=YES;
	return [r objectAtIndex:occ];
}

static BOOL containMonth(NSString *s, int moff)
{
	int yo=0;
	s=[s lowercaseStringRemovingAccents];
	NSCalendarDate *d=[NSCalendarDate calendarDate];
	int nd=[d monthOfYear];
	if (nd<=moff) {
		yo=-1;
		moff+=12;
	} else if (nd+moff>12) {
		yo=+1;
		moff-=12;
	}
	NSDictionary *loc=[[NSUserDefaults standardUserDefaults] dictionaryRepresentation];
	d=[NSCalendarDate dateWithYear:[d yearOfCommonEra]+yo
				 month:[d monthOfYear]+moff
				   day:1
				  hour:0
				minute:0
				second:0
			      timeZone:[d timeZone]];
	NSString *sm=[[d descriptionWithCalendarFormat:@"%B"]lowercaseStringRemovingAccents];
	if ([s hasSubString:sm]) return YES;
	sm=[[d descriptionWithCalendarFormat:@"%B" locale:loc]lowercaseStringRemovingAccents];
	if ([s hasSubString:sm]) return YES;
	sm=[[d descriptionWithCalendarFormat:@"%b"]lowercaseStringRemovingAccents];
	if ([s hasSubString:sm]) return YES;	
	sm=[[d descriptionWithCalendarFormat:@"%b" locale:loc]lowercaseStringRemovingAccents];
	if ([s hasSubString:sm]) return YES;
	return NO;
}

- (id) initWithName:(NSString *)nn 
	     idName:(NSString *)ni
	  className:(NSString *)nc
	     parent:(DOMNode *)p
	       node:(DOMNode *)n
{
	self=[self init];
	if (!self) return self;
	if (![nn length]) nn=nil;
	if (![ni length]) ni=nil;
	if (![nc length]) nc=nil;

	// XXX
	if (nc) ni=nil;
	
	nodeName=[[nn lowercaseString] retain];
	idName=[[ni lowercaseString] retain];
	classNam=[[nc lowercaseString] retain];
	parent=[p retain];
	domNode=[n retain];
	int idx=-1;
	NSArray *matching=[self findMatchingFrom:p sameLevel:YES lookingFor:n counter:0 returnIndex:&idx];
	if (![matching count]) {
		NSLog(@"no match\n");
		//matching=[self findMatchingFrom:p sameLevel:YES lookingFor:n counter:0 returnIndex:&idx];
		return nil;
	}
	//NSLog(@"matched %d item, idx %d\n", [matching count], idx);
	// check for possible specific matching
	if (([matching count]>8) && [n isKindOfClass:[DOMHTMLElement class]]) {
		// check for month
		NSLog(@"possible month selection here\n");
		NSAssert(n==[matching objectAtIndex:idx], @"huh?");
		NSAssert([n isKindOfClass:[DOMHTMLElement class]], @"bad class");
		DOMHTMLElement *e=(DOMHTMLElement *)n;
		if (containMonth([e innerText], 0)
		    && !containMonth([e innerText], -1)			
		    && !containMonth([e innerText], -1)
		    && (!idx || containMonth([[matching objectAtIndex:(idx-1)] innerText], -1))
		    && ((idx>=[matching count]-1) || containMonth([[matching objectAtIndex:(idx+1)] innerText], +1))) {
			    // Month matching
			    specialAction=1;
		}
	}
	occurence=idx;
	return self;
}

- (DOMNode *) domNode
{
	return domNode;
}
- (BOOL) active
{
	return active;
}
- (void) setActive:(BOOL)v
{
	active=v;
	[self updated];

}
- (BOOL) sameLevel
{
	return sameLevel;
}

- (void) setSameLevel:(BOOL)v
{
	[self setSameLevel:v notify:YES];
}

- (void) setSameLevel:(BOOL)v notify:(BOOL)notif
{
	sameLevel=v;
	if (notif) [self updated];
}
- (BOOL) activeCanBeChanged
{
	return activeCanBeChanged;
}
- (void) setActiveCanBeChanged:(BOOL)v
{
	activeCanBeChanged=v;
}

+ (_WebExtractionPathItem *) webExtractionWithName:(NSString *)nn 
				       idName:(NSString *)ni
				    className:(NSString *)nc
				       parent:(DOMNode *)p
					 node:(DOMNode *)n
{
	_WebExtractionPathItem *wi=[[self class] alloc];
	[wi initWithName:nn idName:ni className:nc parent:p node:n];
	return [wi autorelease];
}

- (int) specialAction
{
	return specialAction;
}
- (void) setSpecialAction:(int)v
{
	specialAction=v;
}



- (NSArray *) possibleSpecialActions
{
	static NSArray *spacs=nil;
	if (!spacs) {
		spacs=[[NSArray arrayWithObjects:@"none", 
			@"contain current month", @"contain day of month", @"contain current date",@"contain string", nil]retain];
	}
	return spacs;
}

@end


@implementation WCWebExtractionPath
+ (WCWebExtractionPath *) webExtractionPathForNode:(DOMNode *)n
{
	WCWebExtractionPath *m=[[[self class]alloc]autorelease];
	return [m initWithNode:n];
}

- (void) dealloc {
	[extractionPath release];
	[top release];
	[super dealloc];
}


- (NSString *) description
{
	return [NSString stringWithFormat:@"extraction path with : %@",extractionPath];
}
		
- (id) initWithCoder:(NSCoder *) coder
{
	//self=[super initWithCoder:coder];
	self=[super init];
	if ([coder allowsKeyedCoding]) {
		extractionPath=[[coder decodeObjectForKey:@"extractionPath"]retain];
	}
	return self;
}

- (void)encodeWithCoder:(NSCoder *)coder
{
	//[super encodeWithCoder:coder];
	if ( [coder allowsKeyedCoding] ) {
		[coder encodeObject:extractionPath forKey:@"extractionPath"];
	}
}

- initWithNode:(DOMNode *)n
{
	self=[super init];
	if (self)  {
		extractionPath=[[NSMutableArray arrayWithCapacity:20]retain];
		[self _buildPathFor:n];
	}
	return self;
}

- (void) _buildPath1For:(DOMNode *)n
{
	if (n==top) return;
	DOMNode *p=[n parentNode];
	if (p) [self _buildPath1For:p];
	
	NSString *nn;
	NSString *tagName;
	NSString *idName;
	NSString *className;
	if ([n isKindOfClass:[DOMHTMLElement class]]) {
		DOMHTMLElement *e=(DOMHTMLElement *)n;
		nn=[e nodeName];
		tagName=[e tagName];
		idName=[e idName];
		className=[e className];
	} else {
		NSAssert([n isKindOfClass:[DOMText class]], @"what else?");
		nn=[n nodeName];
		tagName=nn;
		idName=nil;
		className=nil;
	}
	_WebExtractionPathItem *extr=nil;
	if ([[nn lowercaseString] isEqualToString:@"body"]) extr=[_WebExtractionPathItem webExtractionWithName:nn 
										 idName:nil
									      className:nil
										 parent:p node:n];
	else extr=[_WebExtractionPathItem webExtractionWithName:nn 
						    idName:idName
						 className:className
						    parent:p node: n];
	
	if (extr) {
		_WebExtractionPathItem *lastactive=nil;
		DOMNode *lan=p;
		int i, count = [extractionPath count];
		[extractionPath addObject:extr];
		for (i = count-1; i>0; i--, lan=[lan parentNode]) { // dont test item 0 = BODY, always active
			_WebExtractionPathItem * w = [extractionPath objectAtIndex:i];
			if ([w active]) {
				lastactive=w;
				break;
			}
		}// some sites put tbody or not depending on browser id. 
		if (0 && [[nn lowercaseString] isEqualToString:@"tbody"]) {
			[extr setActive:NO];
			[extr setSameLevel:NO];
		} else if (lastactive) {
			int idx1=-1;
			NSArray *a1=[extr findMatchingFrom:p sameLevel:YES lookingFor:n 
					       counter:0 returnIndex:&idx1];
			int idx2=-1;
			DOMNode *p2=[[lastactive domNode] parentNode];
			NSArray *a2=[extr findMatchingFrom:p2  sameLevel:NO lookingFor:n 
						   counter:0 returnIndex:&idx2];
			if ((idx1>=0) && (idx1==idx2) && ([a1 count]==[a2 count])) {
				[lastactive setActive:NO];
				[extr setSameLevel:NO];

			}
		}
		
		// activeCanBeChanged
	}
	
	//NSLog(@"tag %@,id=%@,class=%@\n", tagName, idName, className);
	// ....
}

- (void) _buildPathFor:(DOMNode *)n
{
	NSAssert([extractionPath count]==0, @"should only be invoked at init");
	DOMNode *c=n;
	//NSAssert(!top, @"should only be invoked at init");
	top=c;
	for (;;) {
		DOMNode *p=[c parentNode];
		if (!p) break;
		top=c;
		c=p;
	}
	[top retain];
	[self _buildPath1For:n];
}
- (void) buildPathFor:(DOMNode *)n
{
	[extractionPath release];
	extractionPath=[[NSMutableArray arrayWithCapacity:20]retain];
	[self _buildPathFor:n];
}
- (NSArray *) extractionPath
{
	return extractionPath;
}

- (DOMHTMLElement *)extractFrom:(DOMHTMLElement *)n
{
	unsigned int i, count = [extractionPath count];
	for (i = 0; i < count; i++) {
		_WebExtractionPathItem * wpi = [extractionPath objectAtIndex:i];
		NSAssert([wpi isKindOfClass:[_WebExtractionPathItem class]], @"bad class");
		// fix
		if (!i) [wpi setSameLevel:NO notify:NO];
		n=[wpi extractFrom:n];
		if (!n) break;
	}
	return n;
}

- (NSString *) jsCode
{
	NSString *r=@"";
	unsigned int i, count = [extractionPath count];
	for (i = 0; i < count; i++) {
		_WebExtractionPathItem * pi = [extractionPath objectAtIndex:i];
		r=[r stringByAppendingString:[pi jsCode]];
	}
	return r;
}

@end
